﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace System
{
	public static class Cloner
	{
		private static readonly MethodInfo AddItemsToCollectionT
			= typeof(Cloner).GetMethod("AddItemsToCollection", BindingFlags.Static | BindingFlags.NonPublic);

		private static readonly Dictionary<Type, MethodInfo> cacheAddItemsToCollection
			= new Dictionary<Type, MethodInfo>();


		private static void AddItemsToCollection<T>(IEnumerable<T> orig, ICollection<T> cloned, bool asReference)
		{
			ICloneable cl;
			if (asReference)
			{
				if (CloneScope.HasScope)
					foreach (var item in orig)
						CloneScope.GetOrSuspend(item, n => cloned.Add((T)n));
				else
					foreach (var item in orig)
						cloned.Add(item);

			}
			else
				foreach (var item in orig
						.Select(s => (cl = s as ICloneable) != null ? (T)cl.Clone() : s))
					cloned.Add(item);
		}

		public static T Clone<T>(this T orig)
			where T : class
		{
			return orig.Clone(null);
		}

		public static T Clone<T>(this T orig, T cloned)
			where T: class
		{
			var type = orig.GetType();
			ICloneable cl;
			if (cloned == null)
				cloned = (T)type.CreateInstance();
			foreach (var pair in type.GetPropertiesAndAttribute<SerializeAttribute>(true))
				switch (pair.Value.SerializeAs)
				{
					case SerializeAs.Value:
					case SerializeAs.Element:
					case SerializeAs.Attribute:
						var value = pair.Key.GetValue(orig, TypeExtension.nullPrms);
						if (pair.Value.AsReference)
						{
							if (!CloneScope.GetOrSuspend(value, CachePropSetValue(cloned, pair.Key)))
								pair.Key.SetValue(cloned, value, TypeExtension.nullPrms);
						}
						else
							if (value != null || pair.Value.IgnoreDefault)
							{
								if ((cl = value as ICloneable) != null)
									value = cl.Clone();
								pair.Key.SetValue(cloned, value, TypeExtension.nullPrms);
							}
						break;
					case SerializeAs.Collection:
						var typeitem = pair.Value.TypeItem;
						if (typeitem == null)
							throw new Exception("Аттрибут свойства не определяет тип элемента коллекции");
						var collorig = pair.Key.GetValue(orig, TypeExtension.nullPrms);
						if (collorig != null)
						{
							var collcl = pair.Key.GetValue(cloned, TypeExtension.nullPrms);
							var createdColl = collcl == null;
							if (createdColl)
								collcl = pair.Key.PropertyType.CreateInstance();
							if (collcl != null)
							{
								cacheAddItemsToCollection.GetOrAddDefault(typeitem, CachePropAddItemsToCollectionT(typeitem))
									.Invoke(null, new[] { collorig, collcl, pair.Value.AsReference });
								if (createdColl)
									pair.Key.SetValue(cloned, collcl, TypeExtension.nullPrms);
							}
						}
						break;
					default:
						throw new ArgumentOutOfRangeException();
				}
			CloneScope.Add(orig, cloned);
			return cloned;
		}

		private static Action<object> CachePropSetValue(object obj, PropertyInfo pi)
		{
			return n => pi.SetValue(obj, n, TypeExtension.nullPrms);
		}

		private static Func<MethodInfo> CachePropAddItemsToCollectionT(Type tp)
		{
			return () => AddItemsToCollectionT.MakeGenericMethod(tp);
		}
	}
}
